package de.androidbuch.amando.services.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Location;
import android.net.ConnectivityManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Process;
import android.util.Log;
import de.androidbuch.amando.R;
import de.androidbuch.amando.common.GpsData;
import de.androidbuch.amando.gui.KarteAnzeigen;
import de.androidbuch.amando.kontakt.GeoKontakt;
import de.androidbuch.amando.kontakt.GeoPosition;
import de.androidbuch.amando.services.INetzwerkService;

/**
 * Der Service dient zum Austausch der Geoposition zwischen
 * zwei Android-Geräten über den Server. Nach dem
 * Verschicken einer SMS ("Position senden") mit den
 * Geo-Koordinaten, baut dieser Service eine Verbindung zum
 * Server auf und schickt jedesmal, wenn sich die Position
 * signifikant ändert, eine neue Position zum Server. Als
 * Schlüssel zur Identifizierung des Positionssenders dient
 * die eigene Mobilnummer. Der Empfänger der SMS baut
 * ebenfalls über diesen Service eine Verbindung zum Server
 * auf. Schlüssel für den Empfang aktueller Positionsdaten
 * ist die Mobilnummer des SMS-Senders. Die Verbindung zum
 * Server wird von beiden Parteien dauerhaft gehalten.
 * Bricht die Verbindung weg (Funkloch), wird sie
 * automatisch neu aufgebaut.
 * 
 * @author Arno Becker, 2010 visionera gmbh
 */
public class NetzwerkServiceImpl extends Service {
  
  /** Kuerzel fuers Logging. */
  private static final String TAG = "NetzwerkServiceImpl";

  /** Amando-Server läuft lokal auf diesem Rechner. */
  public static final int SERVER_LOKAL = 1;
  /** Amando-Server läuft auf dem visionera-Server. */
  public static final int SERVER_VISIONERA = 2;

  /** 
   * Zielvariable für Amando-Server: lokal oder 
   * visionera-Server.
   */
  private int mZielServer = SERVER_VISIONERA;

  /** 
   * IP-Adresse des Amando Servers, wenn eine lokale 
   * Serverinstallation verwendet wird.
   */
  private static final String SERVER_IP_LOKAL = 
    "10.0.2.2"; 
 
  /**
   * IP-Adresse des Amando Servers, wenn die visionera- 
   * Serverinstallation verwendet wird.
   */
  private static final String SERVER_IP_EXTERN = 
    "78.46.35.199";

  /** 
   * Portnummer des Amando Servers, wenn eine lokale 
   * Serverinstallation verwendet wird.
   */
  private static final int SOCKET_PORTNUM_LOKAL = 9378; 
      
  /**
   * Portnummer des Amando Servers, wenn die visionera- 
   * Serverinstallation verwendet wird.
   */
  private static final int SOCKET_PORTNUM_EXTERN = 9380; 
                                                  
  /**
   * HTTP-Portnummer des Amando Servers, wenn eine lokale 
   * Serverinstallation verwendet wird.
   */
  private static final int HTTP_PORTNUM_LOKAL = 8081;
  
  /**
   * HTTP-Portnummer des Amando Servers, wenn die 
   * visionera-Serverinstallation verwendet wird.
   */
  private static final int HTTP_PORTNUM_EXTERN = 8082;
  
  /** 
   * Id der Notification, wenn der Service im Vordergrund
   * gestartet wird.
   */
  private static final int NOTIFICATION_ID = 77;
                                                      

  /** IP-Adresse des Amando-Servers. */
  private String mAmandoServerIp = SERVER_IP_EXTERN;
  
  /** Portnummer für die Socket-Verbindungen. */
  private int mSocketPortNum = SOCKET_PORTNUM_EXTERN;
  
  /** Portnummer für die HTTP-Verbindungen. */
  private int mHttpPortNum = HTTP_PORTNUM_EXTERN;
  
  /** URL für die HTTP-Verbindung zum Server. */
  private String mUrlString = "http://" +
      mAmandoServerIp + ":" + mHttpPortNum + 
      "/amandoserver/GeoPositionsService";

  /**
   * Empfaengt Intent, wenn sich der Status des Netzwerks
   * aendert connect/disconnect.
   */
  private ConnectionBroadcastReceiver mBroadcastReceiver;
  
  /**
   * Intent-Filter für abgerissene und wiederhergestellte
   * Netzwerkverbindungen.
   */
  private final IntentFilter mIntentFilter = 
    new IntentFilter(
      "android.net.conn.CONNECTIVITY_CHANGE");

  /** Socket zum Amando-Server. */
  private Socket mSocket;
  
  /** Reader für Socket-Daten vom Server. */
  private volatile BufferedReader mBufferedReader;
  
  /** 
   * Thread zum Lauschen auf neue Positionsdaten
   * bei offen-gehaltener Socket-Verbindung.
   */
  private Thread mPositionListenerThread;

  /** 
   * Verhindert Nebenläufigkeit beim Lesen auf
   * dem Socket.
   */
  private volatile boolean mIsBusy;

  /**
   * true, wenn Positionsdaten vom Server geholt werden
   * sollen.
   */
  private boolean mHolePositionsdaten;

  /**
   * Handler zur Aktualisierung von KarteAnzeigen-Activity,
   * falls neue Positionsdaten vorliegen.
   */
  private Handler mKarteAnzeigenCallbackHandler;
  
  /**
   * Binder für Zugriffe von aussen auf diesen Service.
   */
  private final IBinder mNetzwerkServiceBinder =
    new NetzwerkServiceBinder();

  /**
   * Dieser BroadcastReceiver registriert
   * Netzwerk-Verbindungsabbrueche und wiederhergestellte
   * Netzwerkverbindungen.
   * 
   * @author becker
   */
  private class ConnectionBroadcastReceiver extends
      BroadcastReceiver {
    @Override
    public void onReceive(Context ctxt, Intent intent) {
      try {
        Log.d(TAG, "onReceive(): entered...");
        final boolean isNotConnected =
            intent.getBooleanExtra(
            ConnectivityManager.EXTRA_NO_CONNECTIVITY,
            false);
        if (isNotConnected) {
          Log.d(TAG, "onReceive(): Netzwerkverbindung " +
              "verloren...");
          if (mSocket != null && mSocket.isConnected()) {
            Log.w(TAG, "onReceive(): Netzwerkverbindung " +
                "unterbrochen!");
            mSocket.close();
            // TODO ABE Zeige Warnung, solange keine
            // Netzwerkverbindung besteht
          }
        } else {
          Log.d(TAG, "onReceive(): Verbindung besteht " +
              "wieder. Starte Connection neu...");
          if (mHolePositionsdaten && 
              netzwerkVerbindungHerstellen()) {
            startePositionsdatenListener();
          }
        }
      } catch (IOException e) {
        Log.e(TAG, "Fehler: " + e.toString());
      }
    }
  };

  /**
   * Klassen, die den Binder fuer den Zugriff von Clients
   * auf diesen Service definiert. Da dieser Service immer
   * im gleichen Prozess wie der Aufrufer laeuft, ist kein
   * IPC notwendig.
   * 
   * @param position Position, die an den Server geschickt 
   *     werden soll.
   */
  public class NetzwerkServiceBinder extends Binder
      implements INetzwerkService {
    @Override
    public void sendeEigenePosition(
        final GeoPosition position) {
      Log.d(TAG, "NetzwerkServiceBinder->" +
          "sendePosition(): entered...");

      if (mIsBusy) {
        Log.d(TAG, "NetzwerkServiceBinder->" +
            "sendePosition(): ist busy. Return...");
        return;
      }

      // die Methoden des Binders sind blockierend. Hier
      // muss die möglicherweise
      // länger dauernde Methode sendePosition() in einem
      // Thread aufgerufen werden:
      final Thread thread = new Thread() {
        @Override
        public void run() {
          mIsBusy = true;
          _sendeEigenePosition(position);
          mIsBusy = false;
        }
      };
      thread.start();
    }

    /**
     * Lauscht, ob sich der Besitzer von 'mobilNummer'
     * bewegt. Falls ja, wird dem
     * @see{mKarteAnzeigenCallbackHandler} eine Message mit
     * der neuen Position des Clients übegeben.
     * 
     * @param senderMobilNummer
     *          Mobilnummer des Senders der SMS
     * @param clientMobilNummer
     *          Mobilnummer dieses Geräts (Client)
     * @return true, wenn die Verbindung mit dem Server
     *         klappt. Sonst false
     */
    public boolean empfangeFreundPosition(
        final String senderMobilNummer,
        final String clientMobilNummer) {
      Log.d(TAG, "NetzwerkServiceBinder->" +
          "empfangeFreundPosition(): "
              + "Sender-Mobilnummer: "
              + senderMobilNummer);
      Log.d(TAG, "NetzwerkServiceBinder->" +
          "empfangeFreundPosition(): "
              + "Client-Mobilnummer: "
              + clientMobilNummer);
      mHolePositionsdaten = true;
      if (netzwerkVerbindungHerstellen()) {
        if (registrierePositionsdatenListener(
            senderMobilNummer,
            clientMobilNummer)) {
          startePositionsdatenListener();
          return true;
        }
      }
      return false;
    }

    @Override
    public void setAnzeigeCallbackHandler(
        final Handler callback) {
      mKarteAnzeigenCallbackHandler = callback;
    }

    /**
     * Setzt den Callback-Handler aus Activity
     * KarteAnzeigen auf null.
     */
    public void entferneAnzeigeCallbackHandler() {
      mKarteAnzeigenCallbackHandler = null;
    }
  }

  @Override
  public void onCreate() {
    mBroadcastReceiver = new ConnectionBroadcastReceiver();
    registerReceiver(mBroadcastReceiver, mIntentFilter);
    
    Log.d(TAG, "onCreate(): PID: " + Process.myPid());
    Log.d(TAG, "onCreate(): TID: " + Process.myTid());
    Log.d(TAG, "onCreate(): UID: " + Process.myUid());

    if (mZielServer == SERVER_VISIONERA) {
      mAmandoServerIp = SERVER_IP_EXTERN;
      mSocketPortNum = SOCKET_PORTNUM_EXTERN;
      mHttpPortNum = HTTP_PORTNUM_EXTERN;     
    } else { // lokale Amando-Server Installation
      mAmandoServerIp = SERVER_IP_LOKAL;
      mSocketPortNum = SOCKET_PORTNUM_LOKAL;
      mHttpPortNum = HTTP_PORTNUM_LOKAL;      
    }
    mUrlString = "http://" + mAmandoServerIp + ":" + 
        mHttpPortNum + 
        "/amandoserver/GeoPositionsService";
    
    final Notification notification = new Notification(
        R.drawable.icon, "Läuft!",
        System.currentTimeMillis());
    
    final PendingIntent pendingIntent = 
      PendingIntent.getActivity(this, 0, null, 0);

    notification.setLatestEventInfo(this, 
        "Netzwerk Service",
        "Im Vordergrund gestartet!", pendingIntent); 
    
    // Ab Android 2.0:
    startForeground(NOTIFICATION_ID, notification);
    
    // Bis einschließlich Android 1.6:
    // setForeground(true);
    
    super.onCreate();
  }
  
  /*Override
  public int onStartCommand(Intent intent, int flags, 
      int startId) {
    Log.d(TAG, "onStartCommand(): entered...");
    final Notification notification = new Notification(
        R.drawable.icon, "Test",
        System.currentTimeMillis());
    
    PendingIntent contentIntent = 
      PendingIntent.getActivity(this, 0, null, 0);
 //       new Intent(this, Startseite.class), 0);

    notification.setLatestEventInfo(this, 
       "Local Service Label",
               "Textblabla", contentIntent); 

    
    this.startForeground(12, notification);
    
    return START_STICKY;

  } */
  
  @Override
  public void onDestroy() {
    stoppePositionsdatenListener();
    unregisterReceiver(mBroadcastReceiver);
    super.onDestroy();
  }

  @Override
  public IBinder onBind(Intent intent) {
    Log.d(TAG, "onBind(): entered...");
    return mNetzwerkServiceBinder;
  }

  /**
   * Baut eine Socket-Verbindung zum Amando-Server auf.
   * @return true, falls die Verbindung erfolgreich
   *   aufgebaut wurde.
   */
  private boolean netzwerkVerbindungHerstellen() {
    Log.d(TAG, "netzwerkVerbindungHerstellen(): " +
        "Netzwerkverbindung herstellen...");
    try {
      // Neu: Socket mit Timeout, falls der Server nicht
      // verfuegbar ist:
      Log.d(TAG,
          "netzwerkVerbindungHerstellen(): Server-IP: "
              + mAmandoServerIp);
      Log.d(TAG,
          "netzwerkVerbindungHerstellen(): Portnummer: "
              + mSocketPortNum);
      
      mSocket = new Socket();
      mSocket.connect(new InetSocketAddress(mAmandoServerIp,
          mSocketPortNum), 4000);
      
      mBufferedReader = new BufferedReader(
          new InputStreamReader(mSocket.getInputStream()));
    } catch (UnknownHostException e) {
      Log.e(TAG, "netzwerkVerbindungHerstellen(): "
          + e.toString());
      mSocket = null;
      return false;
    } catch (IOException e) {
      Log.e(TAG, "netzwerkVerbindungHerstellen(): "
          + e.toString());
      mSocket = null;
      return false;
    } 
    return true;
  }

  /**
   * Schickt einen Schlüssel, bestehend aus der eigenen
   * Mobilnummer und aus der Mobilnummer des SMS-Senders 
   * an den Server, um die zu diesem Zeitpunkt schon 
   * bestehende Socket-Verbindung fuer 
   * Positionsveränderungen eines
   * Android-Geräts mit einer bestimmten Mobilnummer zu
   * registrieren.
   * 
   * @param senderMobilNummer Mobilnummer des SMS-Senders
   * @param clientMobilNummer eigene Mobilnummer 
   * @return true, falls der Schlüssel erfolgreich an den
   *   Amando-Server übermittelt werden konnte.
   */
  private boolean registrierePositionsdatenListener(
      final String senderMobilNummer,
      final String clientMobilNummer) {
    Log.d(TAG,
        "registrierePositionsdatenListener(): entered...");
    try {
      // Format: senderNummer#clientNummer
      final String schluessel = senderMobilNummer + "#"
          + clientMobilNummer;
      final PrintWriter pw = new PrintWriter(mSocket
          .getOutputStream(), true);
      pw.println(schluessel);

      return true;
    } catch (IOException e) {
      Log.e(TAG, "registrierePositionsdatenListener():"
          + e.getMessage());
      return false;
    }
  }

  /**
   * Startet einen Thread, der auf eigehende Nachrichten vom
   * Amando-Server wartet. Dieser übermittelt neue 
   * Geopositionen des Bekannten an diesen Listener.
   */
  private void startePositionsdatenListener() {
    Log.d(TAG, "startePositionsdatenListener(): " +
        "entered...");
    mPositionListenerThread = new Thread() {
      @Override
      public void run() {
        try {
          Log.d(TAG, "startePositionsdatenListener()->" +
              "run(): entered...");

          String senderGeoposition;
          // blockiere, bis Verbindung zum Server steht:
          while (!mBufferedReader.ready()) { 
            try {
              Thread.sleep(200);
            } catch (InterruptedException e) { 
              e.printStackTrace();
            }
          }
          Log.d(TAG, "run()-> ready...");

          while ((senderGeoposition = 
                mBufferedReader.readLine()) != null) {
            // Beispiel:
            // 01607644553#37.2345675#49.7634567#
            //   25.63656776#1265385731102
            Log.d(TAG, "run()->Vom Server uebermittelt: "
                + senderGeoposition);

            // nur aktiv, wenn gerade die Activity
            // 'KarteAnzeigen' angezeigt wird:
            if (senderGeoposition != null &&
                !"".equals(senderGeoposition) &&
                mKarteAnzeigenCallbackHandler != null) {
              // FIXME ABE uebertragene Parameter setzen,
              // derzeit Dummy-Werte!
              final String[] parameter = senderGeoposition
                  .split("#");

              final GpsData gpsData = new GpsData(Double
                  .parseDouble(parameter[1]),
                  Double.parseDouble(parameter[2]), Double
                  .parseDouble(parameter[3]),
                  Long.parseLong(parameter[4]));
              final Location ortsPosition = 
                gpsData.toLocation();

              final Bundle bundle = new Bundle();
              bundle.putParcelable(
                  KarteAnzeigen.IN_PARAM_GEO_POSITION,
                  ortsPosition);
              bundle.putString(GeoKontakt.KEY_MOBILNUMMER,
                  parameter[0]);

              final Message msg = new Message();
              msg.setData(bundle);

              mKarteAnzeigenCallbackHandler.
                  sendMessage(msg);
            }
          } // ende while
          Log.d(TAG, "startePositionsdatenListener()->" +
              "run(): Listener beendet...");
          mPositionListenerThread = null;
        } catch (IOException ex) {
          Log.d(TAG, "startePositionsdatenListener->" +
              "run()->Fehler: " + ex.toString());
          mPositionListenerThread = null;
          return;
        }
      }
    };
    mPositionListenerThread.start();
    Log.d(TAG, "startePositionsdatenListener()->" +
        "Gestartet. Lausche auf eingehende " +
        "Positionsmeldungen...");
  }
  
  /**
   * Stoppt den Listener.
   */
  private void stoppePositionsdatenListener() {
    mPositionListenerThread = null;
  }

  /**
   * Versendet die eigene aktuelle Position via HTTP. Die
   * Implementierung nutzt den Apache HttpClient.
   * 
   * @param position
   *          aktuelle eigene Position
   */
  private void _sendeEigenePosition(
      final GeoPosition position) {
    Log.d(TAG, "_sendePosition(): URL: " + mUrlString);

    final DefaultHttpClient client = 
      new DefaultHttpClient();
    final HttpPost httpPost = new HttpPost(mUrlString);

    final List<NameValuePair> postParameters = 
      new ArrayList<NameValuePair>();
    postParameters.add(
        new BasicNameValuePair(GeoKontakt.KEY_MOBILNUMMER,
        position.mMobilnummer));
    postParameters.add(
        new BasicNameValuePair(GpsData.KEY_LAENGENGRAD,
        String.valueOf(position.mGpsData.mLaengengrad)));
    postParameters.add(
        new BasicNameValuePair(GpsData.KEY_BREITENGRAD,
        String.valueOf(position.mGpsData.mBreitengrad)));
    postParameters.add(
        new BasicNameValuePair(GpsData.KEY_HOEHE,
        String.valueOf(position.mGpsData.mHoehe)));
    postParameters.add(
        new BasicNameValuePair(GpsData.KEY_ZEITSTEMPEL,
        String.valueOf(position.mGpsData.mZeitstempel)));
    try {
      httpPost.setEntity(new UrlEncodedFormEntity(
          postParameters));
    } catch (UnsupportedEncodingException e2) {
      Log.e(TAG, e2.getMessage());
    }

    try {
      Log.d(TAG, "_sendePosition(): Request abschicken...");
      client.execute(httpPost);      
    } catch (ClientProtocolException e1) {
      Log.e(TAG, e1.getMessage());
    } catch (IOException e1) {
      Log.e(TAG, e1.getMessage());
    }
    
    Log.d(TAG, "_sendePosition(): leave...");
  }
}
